/**
 *  Copyright (c) Filipe Manana <fdmanana@gmail.com>
 *  All rights reserved.
 * 
 *  This file is part of jsdet.
 *
 *  jsdet is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  jsdet is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with jsdet.  If not, see <http://www.gnu.org/licenses/>.
 *
 */


package jsdet.parsing.helper;


import java.util.List;
import java.util.ArrayList;
import java.util.Stack;

import java.util.regex.Matcher;

import org.antlr.runtime.BitSet;
import org.antlr.runtime.EarlyExitException;
import org.antlr.runtime.FailedPredicateException;
import org.antlr.runtime.IntStream;
import org.antlr.runtime.MismatchedNotSetException;
import org.antlr.runtime.MismatchedSetException;
import org.antlr.runtime.MismatchedTokenException;
import org.antlr.runtime.MismatchedTreeNodeException;
import org.antlr.runtime.MissingTokenException;
import org.antlr.runtime.NoViableAltException;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.UnwantedTokenException;
import org.antlr.runtime.Token;

import jsdet.parsing.ParsingStats;
import jsdet.parsing.enums.MemberType;
import jsdet.parsing.enums.ClassMemberType;
import jsdet.parsing.enums.RightHandSideType;
import jsdet.parsing.structures.FunctionDetails;
import jsdet.parsing.structures.FunctionCallDetails;
import jsdet.parsing.structures.ImmediateFunctionCallDetails;
import jsdet.parsing.structures.NewCallDetails;
import jsdet.parsing.structures.ObjectLiteralDetails;

import jsdet.parsing.regexps.Regexps;

import jsdet.parsing.scopes.VariablesScope;



/**
 * Class to be used only by the JavaScriptParser class (ANTLR generated).
 * 
 * @author Filipe Manana
 *
 */
public class JavaScriptParserHelper {

    private boolean debug = false;
    private List<String> errors = new ArrayList<String>();
    private int parseErrors = 0;

    public Stack<String> paraphrases = new Stack<String>();
    public ParsingStats parsingStats = new ParsingStats();

    ////////////////////////////////////////////////////
    // helper methods for the parsing process follows //
    ////////////////////////////////////////////////////

    public void addClassToStats(String className, FunctionDetails details) {

        for ( int i = 0; i < details.members.size(); i++ ) {
            String member = details.members.get(i);
            MemberType rawType = details.membersType.get(i);
            ClassMemberType type = null;

            switch ( rawType ) {
                case FUNCTION:
                    type = ClassMemberType.FUNCTION;
                    break;
                default:
                    type = ClassMemberType.NON_FUNCTION;
            }

            parsingStats.addClassMember(className, member, type);
        }
    }


    public void addClassToStats(String className, ObjectLiteralDetails details) {

        for ( int i = 0; i < details.members.size(); i++ ) {
            String member = details.members.get(i);
            RightHandSideType type = details.membersType.get(i);

            switch ( type ) {
                case FUNCTION:
                    FunctionDetails funDetails = (FunctionDetails) details.membersValue.get(i);

                    parsingStats.addClassMember(className, member, ClassMemberType.FUNCTION);
                    if ( funDetails.isClass ) {
                        // it's a method declaring other class members
                        addClassToStats(className, funDetails);
                    }
                    break;

                default:
                    parsingStats.addClassMember(className, member, ClassMemberType.NON_FUNCTION);
            }
        }
    }


    public void processFunctionDeclaration(VariablesScope variablesScope, String functionName) {

        if ( (variablesScope.getLevel() != 2) || !variablesScope.isFunctionScope()) {
            return;
        }

        FunctionDetails funDetails = variablesScope.getFunctionDetails();

        if ( !parsingStats.hasClass(functionName) && !funDetails.isClass ) {
            parsingStats.addFunction(functionName);
            return;
        }

        if ( funDetails.isClass ) {
            addClassToStats(functionName, funDetails);
            return;
        }
    }


    public void processDeclaredVariable(VariablesScope variablesScope, String varName, RightHandSideType type, Object value) {

        if ( variablesScope.getLevel() != 1 ) {
            return;   // global scope only
        }

        if ( type == null ) {
            return;
        }

        switch ( type ) {
            case FUNCTION:
                FunctionDetails funDetails = (FunctionDetails) value;
                if ( !funDetails.isClass ) {
                    parsingStats.addFunction(varName);
                } else {
                    addClassToStats(varName, funDetails);
                }
                break;

            case FUNCTION_CALL:
                FunctionCallDetails callDetails = (FunctionCallDetails) value;

                if ( Regexps.matches(callDetails.functionName, Regexps.EXT_EXTEND) && (callDetails.args.names.size() == 2) ) {
                    String arg1 = callDetails.args.names.get(0);

                    if ( !Regexps.matches(arg1, Regexps.IDENT_INDEXED_PATTERN) ) {
                        return;
                    }

                    RightHandSideType arg2Type = callDetails.args.types.get(1);

                    if ( arg2Type != RightHandSideType.OBJECT_LITERAL ) {
                        return;
                    }

                    ObjectLiteralDetails objLiteral = (ObjectLiteralDetails) callDetails.args.values.get(1);
                    addClassToStats(varName, objLiteral);
                    parsingStats.setBaseClass(varName, arg1);
                }
                break;

            case IMMEDIATE_FUNCTION_CALL:
                ImmediateFunctionCallDetails immCallDetails = (ImmediateFunctionCallDetails) value;
                FunctionDetails fDetails = immCallDetails.functionDetails;

                if ( fDetails.returnType != RightHandSideType.OBJECT_LITERAL ) {
                    return;
                }

                parsingStats.addClosureObject(varName);

                ObjectLiteralDetails objLiteral = (ObjectLiteralDetails) fDetails.returnValue;
                for ( int i = 0; i < objLiteral.members.size(); i++ ) {
                    String member = objLiteral.members.get(i);
                    RightHandSideType memberType = objLiteral.membersType.get(i);

                    switch ( memberType ) {
                        case FUNCTION:
                            parsingStats.addClosureObjectMember(varName, member, ClassMemberType.FUNCTION);
                            break;
                        default:
                            parsingStats.addClosureObjectMember(varName, member, ClassMemberType.NON_FUNCTION);
                    }
                }
                break;
        }
    }


    public void processAssignmentExpressionList(VariablesScope variablesScope, List<RightHandSideType> types, List<Object> values) {

        for ( int i = 0; i < types.size(); i++ ) {
            RightHandSideType type = types.get(i);

            switch ( type ) {

                case FUNCTION_CALL:

                    if ( variablesScope.getLevel() != 1 ) {
                        break;
                    }

                    FunctionCallDetails callDetails = (FunctionCallDetails) values.get(i);

                    if ( Regexps.matches(callDetails.functionName, Regexps.EXT_EXTEND) ) {
                        if ( callDetails.args.names.size() == 2 ) {
                            RightHandSideType arg1Type = callDetails.args.types.get(0);

                            if ( arg1Type != RightHandSideType.OTHER ) {
                                break;
                            }

                            String arg1 = callDetails.args.names.get(0);

                            if ( !Regexps.matches(arg1, Regexps.IDENT_INDEXED_PATTERN) ) {
                                break;
                            }

                            RightHandSideType arg2Type = callDetails.args.types.get(1);

                            if ( arg2Type != RightHandSideType.OTHER ) {
                                break;
                            }

                            String arg2 = callDetails.args.names.get(1);

                            if ( !Regexps.matches(arg2, Regexps.IDENT_INDEXED_PATTERN) ) {
                                break;
                            }

                            parsingStats.addClass(arg1);
                            parsingStats.setBaseClass(arg1, arg2);
                        }
                        else if ( callDetails.args.names.size() == 3 ) {
                            RightHandSideType arg1Type = callDetails.args.types.get(0);
                            RightHandSideType arg2Type = callDetails.args.types.get(1);

                            if ( (arg1Type != RightHandSideType.OTHER) || (arg2Type != RightHandSideType.OTHER) ) {
                                break;
                            }

                            RightHandSideType arg3Type = callDetails.args.types.get(2);

                            if ( arg3Type != RightHandSideType.OBJECT_LITERAL ) {
                                break;
                            }

                            String arg1 = callDetails.args.names.get(0);
                            String arg2 = callDetails.args.names.get(1);

                            if ( !Regexps.matches(arg1, Regexps.IDENT_INDEXED_PATTERN) ||
                                    !Regexps.matches(arg2, Regexps.IDENT_INDEXED_PATTERN) ) {
                                break;
                            }

                            ObjectLiteralDetails objLiteral = (ObjectLiteralDetails) callDetails.args.values.get(2);
                            addClassToStats(arg1, objLiteral);
                            parsingStats.setBaseClass(arg1, arg2);
                        }
                    }
                    break;  // case FUNCTION_CALL

            }  // switch ( type )
        }  // for

    }


    public void processAssignment(VariablesScope variablesScope, String leftSideName, RightHandSideType rightSideType, Object rightSideValue) {
        // TODO: this method should be improved a LOT!

        if ( (rightSideType == null)  || (leftSideName == null) ) {
            return;
        }

        Matcher m = null;

        m = Regexps.THIS_MEMBER_PATTERN.matcher(leftSideName);
        if ( variablesScope.isFunctionScope() && (variablesScope.getLevel() == 2) && m.matches() ) {
            // declaration of members within a function (constructor) using style:  this.memberName = value;
            FunctionDetails funDetails = variablesScope.getFunctionDetails();
            String memberName= m.group(1);
            MemberType memberType = MemberType.OTHER;

            switch ( rightSideType ) {
                case FUNCTION:
                    memberType = MemberType.FUNCTION;
                    break;
            }

            funDetails.isClass = true;
            funDetails.members.add(memberName);
            funDetails.membersType.add(memberType);

            return;
        }

        m = Regexps.PROTO_MEMBER_PATTERN.matcher(leftSideName);
        if ( (variablesScope.getLevel() == 1)  &&  m.matches() ) {
            String className = m.group(1);
            String memberName = m.group(2);
            ClassMemberType memberType = null;

            if ( memberName.equals("constructor") ) {
                return;
            }

            switch ( rightSideType ) {
                case FUNCTION:
                    memberType = ClassMemberType.FUNCTION;
                    break;
                default:
                    memberType = ClassMemberType.NON_FUNCTION;
            }

            parsingStats.addClassMember(className, memberName, memberType);

            return;
        }

        m = Regexps.PROTO_PATTERN.matcher(leftSideName);
        if ( (variablesScope.getLevel() == 1)  &&  m.matches() ) {
            String className = m.group(1);

            switch ( rightSideType ) {
                case OBJECT_LITERAL:
                    ObjectLiteralDetails odetails = (ObjectLiteralDetails) rightSideValue;
                    addClassToStats(className, odetails);
                    break;

                case NEW_CALL:
                    NewCallDetails newdetails = (NewCallDetails) rightSideValue;
                    if ( Regexps.matches(newdetails.name, Regexps.IDENT_INDEXED_PATTERN) ) {
                        parsingStats.setBaseClass(className, newdetails.name);
                    }
                    break;
            }

            return;
        }

        m = Regexps.COMPLEX_PROTO_MEMBER_PATTERN.matcher(leftSideName);
        if ( m.matches() ) {
            // TODO: this will be complex...
            return;
        }

        if ( (variablesScope.getLevel() == 1) && Regexps.matches(leftSideName, Regexps.IDENT_INDEXED_PATTERN) ) {

            FunctionDetails funDetails = null;
            switch ( rightSideType ) {
                case FUNCTION:
                    funDetails = (FunctionDetails) rightSideValue;
                    if ( !funDetails.isClass ) {
                        parsingStats.addFunction(leftSideName);
                    } else {
                        addClassToStats(leftSideName, funDetails);
                    }
                    break;

                case FUNCTION_CALL:
                    FunctionCallDetails callDetails = (FunctionCallDetails) rightSideValue;

                    if ( Regexps.matches(callDetails.functionName, Regexps.EXT_EXTEND) && (callDetails.args.names.size() == 2) ) {

                        RightHandSideType arg1Type = callDetails.args.types.get(0);

                        if ( arg1Type != RightHandSideType.OTHER ) {
                            return;
                        }

                        String arg1 = callDetails.args.names.get(0);

                        if ( !Regexps.matches(arg1, Regexps.IDENT_INDEXED_PATTERN) ) {
                            return;
                        }

                        RightHandSideType arg2Type = callDetails.args.types.get(1);

                        if ( arg2Type != RightHandSideType.OBJECT_LITERAL ) {
                            return;
                        }

                        ObjectLiteralDetails objLiteral = (ObjectLiteralDetails) callDetails.args.values.get(1);
                        addClassToStats(leftSideName, objLiteral);
                        parsingStats.setBaseClass(leftSideName, arg1);
                    }
                    break;

                case IMMEDIATE_FUNCTION_CALL:
                    ImmediateFunctionCallDetails immCallDetails = (ImmediateFunctionCallDetails) rightSideValue;
                    funDetails = immCallDetails.functionDetails;

                    if ( funDetails.returnType != RightHandSideType.OBJECT_LITERAL ) {
                        return;
                    }

                    parsingStats.addClosureObject(leftSideName);

                    ObjectLiteralDetails objLiteral = (ObjectLiteralDetails) funDetails.returnValue;
                    for ( int i = 0; i < objLiteral.members.size(); i++ ) {
                        String member = objLiteral.members.get(i);
                        RightHandSideType memberType = objLiteral.membersType.get(i);

                        switch ( memberType ) {
                            case FUNCTION:
                                parsingStats.addClosureObjectMember(leftSideName, member, ClassMemberType.FUNCTION);
                                break;
                            default:
                                parsingStats.addClosureObjectMember(leftSideName, member, ClassMemberType.NON_FUNCTION);
                        }
                    }
                    break;
            }
            return;
        }
    }


    ////////////////////////////////////////////////
    // error handling and reporting stuff follows //
    ////////////////////////////////////////////////


    public Object recoverFromMismatchedToken(IntStream input, int ttype, BitSet follow)
    throws RecognitionException {

        // fail on first syntactic error
        throw new MismatchedTokenException(ttype, input);
    }


    public boolean recoverFromGrammarRuleException(RecognitionException ex) {

        // NOTE: highly experimental

        parseErrors += 1;

        if ( debug ) {
            return false;
        }

        if ( ex instanceof UnwantedTokenException ||
                ex instanceof MissingTokenException ||
                ex instanceof MismatchedTokenException ) {

            // These exception types seem to be the only ones with meaningful information
            // for a user.

            return false;
        }


        return true;
    }


    public void displayRecognitionError(String[] tokenNames, RecognitionException e) {
        String msg = getErrorMessage(e, tokenNames);

        if ( msg != null ) {
            errors.add(msg);
        }
    }


    public String getErrorHeader(RecognitionException e) {
        return "";
    }


    public String getErrorMessage(RecognitionException ex, String[] tokenNames) {
        StringBuffer msg = new StringBuffer("");
        boolean produceErrorMsg = true;

        if ( ex instanceof UnwantedTokenException ) {
            UnwantedTokenException ute = (UnwantedTokenException) ex;
            String tokenName = "<unknown>";
            Token unwantedToken = ute.getUnexpectedToken();

            if ( ute.expecting == Token.EOF ) {
                tokenName = "EOF";
            } else {
                tokenName = tokenNames[ute.expecting];
            }

            msg.append("extraneous input");

            if ( unwantedToken != null ) {
                msg.append(" ");
                msg.append(unwantedToken.getText());
                msg.append(" expecting ");
                msg.append(tokenName);
            }
        }

        else if ( ex instanceof MissingTokenException ) {
            MissingTokenException mte = (MissingTokenException) ex;
            String tokenName = "<unknown>";

            if ( mte.expecting == Token.EOF ) {
                tokenName = "EOF";
            } else {
                tokenName = tokenNames[mte.expecting];
            }

            msg.append("missing ");
            msg.append(tokenName);

            if ( ex.token != null ) {
                msg.append(" at ");
                msg.append(ex.token.getText());
            }
        }

        else if ( ex instanceof MismatchedTokenException ) {
            MismatchedTokenException mte = (MismatchedTokenException) ex;
            String tokenName = "<unknown>";

            if ( mte.expecting == Token.EOF ) {
                tokenName = "EOF";
            } else {
                tokenName = tokenNames[mte.expecting];
            }

            msg.append("found ");
            msg.append(ex.token.getText());
            msg.append(" but expected ");
            msg.append(tokenName);
        }

        else if ( ex instanceof MismatchedTreeNodeException ) {
            MismatchedTreeNodeException mtne = (MismatchedTreeNodeException) ex;
            String tokenName = "<unknown>";

            if ( mtne.expecting == Token.EOF ) {
                tokenName = "EOF";
            } else {
                tokenName = tokenNames[mtne.expecting];
            }

            msg.append("mismatched tree node: ");
            msg.append(mtne.node);
            msg.append(" expecting ");
            msg.append(tokenName);
        }

        else if ( ex instanceof NoViableAltException ) {
            if ( debug ) {
                NoViableAltException nvae = (NoViableAltException) ex;

                msg.append("no viable alternative at input ");
                msg.append(ex.token.getText());
                msg.append(", decision=<< description: ");
                msg.append(nvae.grammarDecisionDescription);
                msg.append(", number: ");
                msg.append(nvae.decisionNumber);
                msg.append(" >> state=<< ");
                msg.append(nvae.stateNumber);
                msg.append(" >>");
            }
            else {
                produceErrorMsg = false;
            }
        }

        else if ( ex instanceof EarlyExitException ) {
            if ( debug ) {
                EarlyExitException eee = (EarlyExitException) ex;

                msg.append("required (...)+ loop did not match anything at input ");
                msg.append(ex.token.getText());
                msg.append(" - ");
                msg.append("decision ");
                msg.append(eee.decisionNumber);
            }
            else {
                produceErrorMsg = false;
            }
        }

        else if ( ex instanceof MismatchedSetException ) {
            if ( debug ) {
                MismatchedSetException mse = (MismatchedSetException) ex;

                msg.append("mismatched input ");
                msg.append(ex.token.getText());
                msg.append(" expecting set ");
                msg.append(mse.expecting);
            }
            else {
                produceErrorMsg = false;
            }
        }

        else if ( ex instanceof MismatchedNotSetException ) {
            if ( debug ) {
                MismatchedNotSetException mse = (MismatchedNotSetException) ex;

                msg.append("mismatched input ");
                msg.append(ex.token.getText());
                msg.append(" expecting set ");
                msg.append(mse.expecting);
            }
            else {
                produceErrorMsg = false;
            }
        }

        else if ( ex instanceof FailedPredicateException ) {
            if ( debug ) {
                FailedPredicateException fpe = (FailedPredicateException) ex;

                msg.append("rule ");
                msg.append(fpe.ruleName);
                msg.append(" failed predicate: {");
                msg.append(fpe.predicateText);
                msg.append("}?");
            }
            else {
                produceErrorMsg = false;
            }
        }


        if (produceErrorMsg) {
            String prefix = getErrorMessagePrefix(ex, tokenNames);

            if (msg.length() > 0) {
                StringBuffer suffix = msg;

                msg = new StringBuffer(prefix);
                msg.append(suffix);
            } else {
                msg = new StringBuffer(prefix);
                msg.append("unidentifiable cause");
            }
        }

        return ( produceErrorMsg ? msg.toString() : null );
    }


    private String getErrorMessagePrefix(RecognitionException ex, String[] tokenNames) {
        StringBuffer msg = new StringBuffer("");

        msg.append("Error at line ");
        msg.append(ex.line);
        msg.append(", column ");
        msg.append(ex.charPositionInLine);

        if ( !paraphrases.empty() ) {
            String paraphrase = paraphrases.peek();
            msg.append(" ");
            msg.append(paraphrase);
            msg.append(": ");
        }

        return msg.toString();
    }


    public List<String> getErrors() {
        return errors;
    }


    public boolean hasErrors() {
        return parseErrors > 0;
    }


    public boolean isDebug() {
        return debug;
    }


    public void setDebug(boolean debug) {
        this.debug = debug;
    }
}
